# Copyright (c) 2017 Huawei Tech. Co., Ltd. .
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import copy

import netaddr
from neutron_lib import constants as common_const
from os_ken.lib import mac as os_ken_mac_lib
from os_ken.lib.packet import ethernet
from os_ken.lib.packet import icmp
from os_ken.lib.packet import packet
from os_ken.lib.packet import tcp
from os_ken.lib.packet import udp
from os_ken.ofproto import ether
from oslo_log import log

from dragonflow.common import exceptions
from dragonflow.common import utils as df_utils
from dragonflow import conf as cfg
from dragonflow.controller.common import arp_responder
from dragonflow.controller.common import constants as const
from dragonflow.controller.common import icmp_error_generator
from dragonflow.controller.common import icmp_responder
from dragonflow.controller import df_base_app
from dragonflow.db.models import constants as model_constants
from dragonflow.db.models import host_route
from dragonflow.db.models import l2
from dragonflow.db.models import l3


ROUTE_TO_ADD = 'route_to_add'
ROUTE_ADDED = 'route_added'
LOG = log.getLogger(__name__)


class L3AppMixin(object):

    def __init__(self, *args, **kwargs):
        super(L3AppMixin, self).__init__()
        self.route_cache = {}

        self.conf = cfg.CONF.df_l3_app
        self.ttl_invalid_handler_rate_limit = df_utils.RateLimiter(
            max_rate=self.conf.router_ttl_invalid_max_rate,
            time_unit=1)
        self.port_icmp_unreach_respond_rate_limit = df_utils.RateLimiter(
            max_rate=self.conf.router_port_unreach_max_rate,
            time_unit=1)
        self.api.register_table_handler(const.L3_LOOKUP_TABLE,
                                        self.packet_in_handler)

    def switch_features_handler(self, ev):
        self.route_cache.clear()

    def _handle_ttl_expired(self, msg):
        """
        This callback is called when the OVS switch reduced a packet's TTL
        to 0.

        Create an ICMP error packet, and return it.

        :param msg: Packet in message
        :type msg:  os_ken.ofproto.ofproto_v<version>_parser.OFPPacketIn
        """
        if self.ttl_invalid_handler_rate_limit():
            LOG.warning("Get more than %(rate)s TTL invalid packets per "
                        "second at table %(table)s",
                        {'rate': self.conf.router_ttl_invalid_max_rate,
                         'table': const.L3_LOOKUP_TABLE})
            return

        LOG.debug("Get an invalid TTL packet at table %s",
                  const.L3_LOOKUP_TABLE)

        pkt = packet.Packet(msg.data)
        e_pkt = pkt.get_protocol(ethernet.ethernet)
        router_key = msg.match.get('reg5')
        lrouter = self.db_store.get_one(
            l3.LogicalRouter(unique_key=router_key),
            index=l3.LogicalRouter.get_index('unique_key'),
        )
        router_port_ip = None
        for port in lrouter.ports:
            if port.lswitch.unique_key == msg.match.get('metadata'):
                router_port_ip = port.network.ip
                break

        if router_port_ip:
            icmp_ttl_pkt = icmp_error_generator.generate(
                icmp.ICMP_TIME_EXCEEDED, icmp.ICMP_TTL_EXPIRED_CODE,
                msg.data, str(router_port_ip), pkt)
            unique_key = msg.match.get('reg6')
            self.dispatch_packet(icmp_ttl_pkt, unique_key)
        else:
            LOG.warning("The invalid TTL packet's destination mac %s "
                        "can't be recognized.", e_pkt.dst)

    def _handle_invalid_dest(self, msg):
        """
        Handle a packet sent to the router interface (by IP). Since only ping
        and ARP are supported, everything else is responded to with a
        Destination Unreachable message.

        :param msg: Packet in message
        :type msg:  os_ken.ofproto.ofproto_v<version>_parser.OFPPacketIn
        """
        # If the destination is router interface, the unique key of router
        # interface will be set to reg7 before sending to local controller.
        # Code will hit here only when the router interface is not
        # concrete.
        if self.port_icmp_unreach_respond_rate_limit():
            LOG.warning(
                "Get more than %(rate)s packets to router port "
                "per second at table %(table)s",
                {'rate': self.conf.router_port_unreach_max_rate,
                 'table': const.L3_LOOKUP_TABLE})
            return

        # Response icmp unreachable to udp or tcp.
        pkt = packet.Packet(msg.data)
        tcp_pkt = pkt.get_protocol(tcp.tcp)
        udp_pkt = pkt.get_protocol(udp.udp)
        if tcp_pkt or udp_pkt:
            icmp_dst_unreach = icmp_error_generator.generate(
                icmp.ICMP_DEST_UNREACH, icmp.ICMP_PORT_UNREACH_CODE,
                msg.data, pkt=pkt)
            unique_key = msg.match.get('reg6')
            self.dispatch_packet(icmp_dst_unreach, unique_key)

    def router_function_packet_in_handler(self, msg):
        """React to packet as what a normal router will do.

        TTL invalid and router port response will be handled in this method.
        Return True if the packet is handled, so there is no need for further
        handle.

        :param msg: Packet in message
        :type msg:  os_ken.ofproto.ofproto_v<version>_parser.OFPPacketIn
        """

        if msg.reason == self.ofproto.OFPR_INVALID_TTL:
            self._handle_ttl_expired(msg)
        elif msg.match.get('reg7'):
            self._handle_invalid_dest(msg)
        else:
            return False

        return True

    @df_base_app.register_event(l3.LogicalRouter,
                                model_constants.EVENT_CREATED)
    def router_created(self, router):
        for new_port in router.ports:
            self._add_new_router_port(router, new_port)
        for route in router.routes:
            self._add_router_extra_route(router, route)

    @df_base_app.register_event(l3.LogicalRouter,
                                model_constants.EVENT_UPDATED)
    def router_updated(self, router, original_router=None):
        self._update_router_interfaces(original_router, router)
        self._update_router_attributes(original_router, router)

    @df_base_app.register_event(l3.LogicalRouter,
                                model_constants.EVENT_DELETED)
    def router_deleted(self, router):
        for port in router.ports:
            self._delete_router_port(router, port)
        for route in router.routes:
            self._delete_router_extra_route(router, route)
        self.route_cache.pop(router.id, None)

    def _update_router_interfaces(self, old_router, new_router):
        """
        A router has been updated. Delete old router ports, and create new
        router ports
        :param old_router:  The old router instance
        :type old_router:   LogicalRouter model
        :param new_router:  The new router instance
        :type new_router:   LogicalRouter model
        """
        old_ports = old_router.ports
        new_ports = new_router.ports
        for old_port in old_ports:
            if old_port not in new_ports:
                self._delete_router_port(new_router, old_port)
        for new_port in new_ports:
            if new_port not in old_ports:
                self._add_new_router_port(new_router, new_port)

    def _update_router_attributes(self, old_router, new_router):
        """
        A router has been updated. Update the followin attributes:
        * extra routes
        :param old_router:  The old router instance
        :type old_router:   LogicalRouter model
        :param new_router:  The new router instance
        :type new_router:   LogicalRouter model
        """
        old_routes = old_router.routes
        new_routes = new_router.routes
        for old_route in old_routes:
            if old_route not in new_routes:
                self._delete_router_extra_route(new_router, old_route)
        for new_route in new_routes:
            if new_route not in old_routes:
                self._add_router_extra_route(new_router, new_route)

    def _get_port_by_lswitch_and_ip(self, ip, lswitch_id):
        """
        Return the logical port with the given IP that's attached to the given
        Logical Switch.
        :param ip:          The port's IP
        :type ip:           netaddr.IPAddress (or representation thererof)
        :param lswitch_id:  The Logical Switch's ID
        :type lswitch_id:   String
        :return:            LogicalPort or None
        """
        ip_lswitch_idx = l2.LogicalPort.get_index('ip,lswitch')
        ports = self.db_store.get_all(l2.LogicalPort(lswitch=lswitch_id,
                                                     ips=[ip]),
                                      index=ip_lswitch_idx)
        return next(ports, None)

    def _get_gateway_port_by_ip(self, router, ip):
        """
        Return the router port that has the given IP. Raise an exception if
        no such router port exists
        :param router:  The router
        :type router:   LogicalRouter
        :param ip:      The IP to search
        :type ip:       netaddr.IPAddress (or representation thereof)
        :return:        LogicalPort or None
        :raises:        exceptions.DBStoreRecordNotFound
        """
        for port in router.ports:
            if ip in port.network:
                return port

        # Code is not expected to hit here as neutron will prevent from adding
        # unreachable route.
        raise exceptions.DBStoreRecordNotFound(
            record='RouterPort(router=%s, ip=%s)' % (router.name, ip))

    def _add_router_extra_route(self, router, route):
        """Add extra route to router."""

        LOG.debug('Add extra route %(route)s to router %(router)s',
                  {'route': route, 'router': router})

        router_port = self._get_gateway_port_by_ip(router, route.nexthop)
        lport = self._get_port_by_lswitch_and_ip(route.nexthop,
                                                 router_port.lswitch.id)
        router_id = router.id
        if not lport:
            LOG.debug("lport with IP %s doesn't exist, skip adding "
                      "extra route.", route.nexthop)
            self._add_to_route_cache(ROUTE_TO_ADD, router_id, route)
            return

        self._add_extra_route_to_router(router.unique_key,
                                        router_port.mac,
                                        lport.unique_key,
                                        lport.mac, route)
        self._add_to_route_cache(ROUTE_ADDED, router_id, route)

    def _delete_router_extra_route(self, router, route):
        """Delete extra route from router."""

        LOG.debug('Delete extra route %(route)s from router %(router)s',
                  {'route': route, 'router': router})

        router_port = self._get_gateway_port_by_ip(router, route.nexthop)
        router_unique_key = router.unique_key
        router_if_mac = router_port.mac
        # Delete the openflow for extra route anyway.
        self._delete_extra_route_from_router(router_unique_key,
                                             router_if_mac, route)
        self._del_from_route_cache(ROUTE_ADDED, router.id, route)
        self._del_from_route_cache(ROUTE_TO_ADD, router.id, route)

    def _add_extra_route_to_router(self, router_unique_key, router_if_mac,
                                   lport_unique_key, lport_mac, route):
        """Add extra route to router.
        @param router_unique_key: The unique_key of router where the extra
                                  route belongs to
        @param router_if_mac: The mac address of related router port
        @param lport_unique_key: The unique_key of lport whick will act as
                                 nexthop.
        @param lport_mac: The mac address of lport which will act as nexthop
        @param route: The extra route dict
        """
        LOG.info('Add extra route %s to router', route)

        ofproto = self.ofproto
        parser = self.parser

        # Install openflow entry for extra route, only packets come from
        # the same subnet as nexthop port can use extra route.
        # Match: ip, reg5=router_unique_key, dl_dst=router_if_mac,
        #        nw_dst=destination,
        # Actions:ttl-1, mod_dl_src=router_if_mac, mod_dl_dst=lport_mac,
        #         load_reg7=next_hop_port_key,
        # goto: egress_table
        match = self._generate_extra_route_match(router_unique_key,
                                                 router_if_mac,
                                                 route.destination)

        actions = [
            parser.OFPActionDecNwTtl(),
            parser.OFPActionSetField(eth_src=router_if_mac),
            parser.OFPActionSetField(eth_dst=lport_mac),
            parser.OFPActionSetField(reg7=lport_unique_key),
        ]
        action_inst = parser.OFPInstructionActions(
            ofproto.OFPIT_APPLY_ACTIONS, actions)
        goto_inst = parser.OFPInstructionGotoTable(const.EGRESS_TABLE)
        inst = [action_inst, goto_inst]
        self.mod_flow(
            inst=inst,
            table_id=const.L3_LOOKUP_TABLE,
            priority=const.PRIORITY_VERY_HIGH,
            match=match)

    def _delete_extra_route_from_router(self, router_unique_key,
                                        router_if_mac, route):
        """Delete extra route from router.
        @param router_unique_key: The unique_key of router where the extra
                                  route belongs to
        @param router_if_mac: The mac address of related router port
        @param route: The extra route dict
        """
        LOG.info('Delete extra route %s from router', route)

        ofproto = self.ofproto

        # Remove openflow entry for extra route
        # Match: ip, reg5=router_unique_key, dl_dst=router_if_mac,
        #        nw_dst=destination
        match = self._generate_extra_route_match(router_unique_key,
                                                 router_if_mac,
                                                 route.destination)

        self.mod_flow(
            command=ofproto.OFPFC_DELETE_STRICT,
            table_id=const.L3_LOOKUP_TABLE,
            priority=const.PRIORITY_VERY_HIGH,
            match=match)

    def _generate_extra_route_match(self, router_unique_key, router_if_mac,
                                    destination):
        """
        Create an OpenFlow Match object for the extra route on the router
        :param router_unique_key:   The Unique Key of the router
        :type router_unique_key:    Integer
        :param router_if_mac:       The MAC address of the router
        :type router_if_mac:        netaddr.EUI (or representation thereof)
        :param destination:         The destination network
        :type destination:          netaddr.IPNetwork (or string represenation)
        :return:                    OFPMatch object
        """
        dst_network = destination.network
        dst_netmask = destination.netmask
        if destination.version == common_const.IP_VERSION_4:
            match = self.parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
                                         reg5=router_unique_key,
                                         eth_dst=router_if_mac,
                                         ipv4_dst=(dst_network, dst_netmask))
        else:
            match = self.parser.OFPMatch(eth_type=ether.ETH_TYPE_IPV6,
                                         reg5=router_unique_key,
                                         eth_dst=router_if_mac,
                                         ipv6_dst=(dst_network, dst_netmask))
        return match

    # route cache got following structure
    # {router: {ROUTE_ADDED: set(route), ROUTE_TO_ADD: set(route)}
    def _add_to_route_cache(self, key, router_id, route):
        """
        Update the route_cache dictionary.

        The route_cache dictionary contains two sets per router:
        1. Routes that were added
        2. Routes that are pending (e.g. the relevant port isn't online yet)

        Add the route 'route' to the set 'key' in route_cache for the router
        given by router_id.
        :param key:         The type of route (added, pending)
        :type key:          one of: ROUTE_ADDED, ROUTE_TO_ADD
        :param router_id:   The ID of the router owning the route
        :type router_id:    String
        :param route:       The route
        :type route:        HostRoute model instance
        """
        cached_routes = self.route_cache.get(router_id)
        if cached_routes is None:
            cached_routes = {ROUTE_ADDED: set(), ROUTE_TO_ADD: set()}
            self.route_cache[router_id] = cached_routes
        routes = cached_routes.get(key)
        routes.add((str(route.destination), str(route.nexthop)))

    def _del_from_route_cache(self, key, router_id, route):
        """
        Delete the given route (route) of the given type (key) from the
        route_cache for the given router (router_id). See method
        #_add_to_route_cache for more info about route_cache.
        :param key:         The type of route (added, pending)
        :type key:          one of: ROUTE_ADDED, ROUTE_TO_ADD
        :param router_id:   The ID of the router owning the route
        :type router_id:    String
        :param route:       The route
        :type route:        HostRoute model instance
        """
        cached_routes = self.route_cache.get(router_id)
        if cached_routes is None:
            return
        routes = cached_routes.get(key)
        routes.discard((str(route.destination), str(route.nexthop)))

    def _change_route_cache_status(self, router_id, from_part, to_part, route):
        """Change the status of extra route in cache of app.
        See method #_add_to_route_cache for more info about route_cache.

        :param router_id:   The ID of the router owning the route
        :type router_id:    String
        :param from_part:   The old type of route (added, pending)
        :type from_part:    one of: ROUTE_ADDED, ROUTE_TO_ADD
        :param to_part:     The new type of route (added, pending)
        :type to_part:      one of: ROUTE_ADDED, ROUTE_TO_ADD
        :param route:       The route
        :type route:        HostRoute model instance
        """
        self._del_from_route_cache(from_part, router_id, route)
        self._add_to_route_cache(to_part, router_id, route)

    def _get_router_interface_match(self, router_unique_key, rif_ip):
        """
        Create an OpenFlow Match object for the router interface on the router.
        Used to either pass the packet to the router (concrete router port)
        or send to controller (distributed router port)
        :param router_unique_key:   The Unique Key of the router
        :type router_unique_key:    Integer
        :param rif_ip:              The IP of the router interface
        :type rif_ip:               netaddr.IPAddress (or representation)
        :return:                    OFPMatch object
        """
        if netaddr.IPAddress(rif_ip).version == common_const.IP_VERSION_4:
            return self.parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
                                        reg5=router_unique_key,
                                        ipv4_dst=rif_ip)

        return self.parser.OFPMatch(eth_type=ether.ETH_TYPE_IPV6,
                                    reg5=router_unique_key,
                                    ipv6_dst=rif_ip)

    def _get_router_route_match(self, router_unique_key, destination):
        """
        Create an OpenFlow Match object for a routing entry on the router
        :param router_unique_key:   The Unique Key of the router
        :type router_unique_key:    Integer
        :param destination:         The destination network
        :type destination:          netaddr.IPNetwork (or string represenation)
        :return:                    OFPMatch object
        """
        dst_network = destination.network
        dst_netmask = destination.netmask
        parser = self.parser

        if netaddr.IPAddress(dst_network).version == common_const.IP_VERSION_4:
            match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
                                    reg5=router_unique_key,
                                    ipv4_dst=(dst_network, dst_netmask))
        else:
            match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IPV6,
                                    reg5=router_unique_key,
                                    ipv6_dst=(dst_network, dst_netmask))

        return match

    def _add_new_router_port(self, router, router_port):
        """
        Handle the creation of a new router interface on the router.
        * Match L2 address and update reg5
        * Install ARP and ICMP responders
        * Match packets with router as dst
        * Add flows for  new route entries
        :param router:        The router on which the interface is added
        :type router:         LogicalRouter model object
        :param router_port:   The router interface being added
        :type router_port:    RouterInterface model object
        """
        LOG.info("Adding new logical router interface = %r",
                 router_port)
        local_network_id = router_port.lswitch.unique_key

        parser = self.parser
        ofproto = self.ofproto

        mac = router_port.mac
        router_unique_key = router.unique_key
        dst_ip = router_port.network.ip
        is_ipv4 = (netaddr.IPAddress(dst_ip).version ==
                   common_const.IP_VERSION_4)

        # Add rule for making packets go from L2_LOOKUP_TABLE
        # to L3_LOOKUP_TABLE
        match = parser.OFPMatch()
        match.set_metadata(local_network_id)
        match.set_dl_dst(os_ken_mac_lib.haddr_to_bin(mac))
        actions = [parser.OFPActionSetField(reg5=router_unique_key)]
        action_inst = parser.OFPInstructionActions(
            ofproto.OFPIT_APPLY_ACTIONS, actions)
        goto_inst = parser.OFPInstructionGotoTable(const.L3_LOOKUP_TABLE)
        inst = [action_inst, goto_inst]
        self.mod_flow(
            inst=inst,
            table_id=const.L2_LOOKUP_TABLE,
            priority=const.PRIORITY_HIGH,
            match=match)

        # Add router ARP & ICMP responder for IPv4 Addresses
        if is_ipv4:
            arp_responder.ArpResponder(self,
                                       local_network_id,
                                       dst_ip, mac).add()
            icmp_responder.ICMPResponder(self,
                                         dst_ip,
                                         router_key=router_unique_key).add()

        # If router interface is not concrete, send to local controller. local
        # controller will create icmp unreachable message. A virtual router
        # interface will not be in local cache, as it doesn't have chassis
        # information.
        lport = self.db_store.get_one(l2.LogicalPort(id=router_port.id))
        if not lport:
            match = self._get_router_interface_match(router_unique_key, dst_ip)
            actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                              ofproto.OFPCML_NO_BUFFER)]
            action_inst = parser.OFPInstructionActions(
                ofproto.OFPIT_APPLY_ACTIONS, actions)
            self.mod_flow(
                inst=[action_inst],
                table_id=const.L3_LOOKUP_TABLE,
                priority=const.PRIORITY_HIGH,
                match=match)
        else:
            self._add_concrete_router_interface(lport, router=router)

        # Add rule for routing packets to subnet of this router port
        match = self._get_router_route_match(router_unique_key,
                                             router_port.network)
        self._add_subnet_send_to_route(match, local_network_id, router_port)

    def _delete_router_port(self, router, router_port):
        """
        Handle the removal of a router interface from a router. Undoes the
        actions in #_add_new_router_port.
        :param router:        The router on which the interface is removed
        :type router:         LogicalRouter model object
        :param router_port:   The router interface being removed
        :type router_port:    RouterInterface model object
        """
        LOG.info("Removing logical router interface = %s",
                 router_port)
        local_network_id = router_port.lswitch.unique_key

        parser = self.parser
        ofproto = self.ofproto
        router_unique_key = router.unique_key
        ip = router_port.network.ip
        mac = router_port.mac

        # Delete rule for making packets go from L2_LOOKUP_TABLE
        # to L3_LOOKUP_TABLE
        match = parser.OFPMatch()
        match.set_metadata(local_network_id)
        match.set_dl_dst(os_ken_mac_lib.haddr_to_bin(mac))
        self.mod_flow(
            table_id=const.L2_LOOKUP_TABLE,
            command=ofproto.OFPFC_DELETE,
            priority=const.PRIORITY_HIGH,
            match=match)

        # Delete ARP & ICMP responder for router interface
        if ip.version == common_const.IP_VERSION_4:
            arp_responder.ArpResponder(self, local_network_id, ip).remove()
            icmp_responder.ICMPResponder(self, ip,
                                         router_key=router_unique_key).remove()

        # Delete rule for packets whose destination is router interface.
        match = self._get_router_interface_match(router_unique_key, ip)
        self.mod_flow(
            table_id=const.L3_LOOKUP_TABLE,
            command=ofproto.OFPFC_DELETE,
            priority=const.PRIORITY_HIGH,
            match=match)

        # Delete rule for routing packets to subnet of this router port
        match = self._get_router_route_match(router_unique_key,
                                             router_port.network)
        self.mod_flow(
            table_id=const.L3_LOOKUP_TABLE,
            command=ofproto.OFPFC_DELETE,
            priority=const.PRIORITY_MEDIUM,
            match=match)

    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_BIND_LOCAL)
    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_BIND_REMOTE)
    def _add_port_event_handler(self, lport):
        LOG.debug('add %(locality)s port: %(lport)s',
                  {'lport': lport,
                   'locality': 'local' if lport.is_local else 'remote'})
        if lport.device_owner == common_const.DEVICE_OWNER_ROUTER_INTF:
            self._add_concrete_router_interface(lport)
        else:
            self._add_port(lport)

    def _add_concrete_router_interface(self, lport, router=None):
        """
        The router interace is concrete, direct the packets to the real
        port of router interface. The flow here will overwrite
        the flow that packet-in the packets to local controller.

        If the router is not given (or is None), try to get it from the
        port's owner.

        :param lport:   The router interface's concrete port
        :type lport:    LogicalPort model object
        :param router:  The owning router
        :type lport:    LogicalRouter or None
        """
        router = router or self.db_store.get_one(
            l3.LogicalRouter(id=lport.device_id))
        if not router:
            return

        router_unique_key = router.unique_key
        port_unique_key = lport.unique_key
        match = self._get_router_interface_match(router_unique_key, lport.ip)
        actions = [self.parser.OFPActionSetField(reg7=port_unique_key)]
        action_inst = self.parser.OFPInstructionActions(
            self.ofproto.OFPIT_APPLY_ACTIONS, actions)
        goto_inst = self.parser.OFPInstructionGotoTable(
            const.EGRESS_TABLE)
        inst = [action_inst, goto_inst]
        self.mod_flow(
            inst=inst,
            table_id=const.L3_LOOKUP_TABLE,
            priority=const.PRIORITY_HIGH,
            match=match)

    def _get_router_by_lswitch_and_port_ip(self, lswitch_id, port_ip):
        """Find and return the logical router that lport connects to.

        @param lswitch_id: The lswitch id of lport
        @param port_ip: The ip of lport
        @return Router and the router port that is the gateway of lport
        """
        for router in self.db_store.get_all(l3.LogicalRouter):
            for port in router.ports:
                if (lswitch_id == port.lswitch.id and
                        netaddr.IPAddress(port_ip) in port.network):
                    return router, port
        return None, None

    def _reprocess_to_add_route(self, lport):
        """Add extra routes for lport.

        @param lport: The lport related to extra routes.
        """
        LOG.debug("Reprocess to add extra routes that use lport %s "
                  "as nexthop", lport)
        lswitch_id = lport.lswitch.id
        port_ip = lport.ip
        router, router_if = self._get_router_by_lswitch_and_port_ip(
            lswitch_id, port_ip)
        if not router:
            LOG.debug("No router for lport %s, skip adding extra route",
                      lport)
            return

        router_id = router.id
        cached_routes = self.route_cache.get(router_id)
        if not cached_routes or not cached_routes.get(ROUTE_TO_ADD):
            LOG.debug("No extra routes need to be processed for logical "
                      "router %s", router)
            return

        # Make a copy here, or else _change_route_cache_status will delete
        # elements in routes inside the iteration.
        routes = copy.deepcopy(cached_routes.get(ROUTE_TO_ADD))
        for route in routes:
            if str(port_ip) != route[1]:
                continue
            route = host_route.HostRoute(destination=route[0],
                                         nexthop=route[1])
            self._add_extra_route_to_router(router.unique_key,
                                            router_if.mac,
                                            lport.unique_key,
                                            lport.mac,
                                            route)
            self._change_route_cache_status(router_id,
                                            from_part=ROUTE_TO_ADD,
                                            to_part=ROUTE_ADDED,
                                            route=route)

    def _reprocess_to_delete_route(self, lport):
        """Delete extra routes for lport.

        @param lport: The lport related to extra routes.
        """
        LOG.debug("Reprocess to delete extra routes that use lport %s "
                  "as nexthop", lport)
        lswitch_id = lport.lswitch.id
        port_ip = lport.ip
        router, router_if = self._get_router_by_lswitch_and_port_ip(
            lswitch_id, port_ip)
        if not router:
            LOG.debug("No router for lport %s, skip adding extra route",
                      lport)
            return

        router_id = router.id
        cached_routes = self.route_cache.get(router_id)
        if not cached_routes or not cached_routes.get(ROUTE_ADDED):
            LOG.debug("No extra routes need to be processed for logical "
                      "router %s", router)
            return

        # Make a copy here, or else _change_route_cache_status will delete
        # elements in routes inside the iteration.
        routes = copy.deepcopy(cached_routes.get(ROUTE_ADDED))
        for route in routes:
            if str(port_ip) != route[1]:
                continue
            route = host_route.HostRoute(destination=route[0],
                                         nexthop=route[1])
            self._delete_extra_route_from_router(router.unique_key,
                                                 router_if.mac,
                                                 route)
            self._change_route_cache_status(router_id,
                                            from_part=ROUTE_ADDED,
                                            to_part=ROUTE_TO_ADD,
                                            route=route)

    def _add_port(self, lport):
        """Add port which is not a router interface."""
        self._reprocess_to_add_route(lport)

    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_UNBIND_LOCAL)
    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_UNBIND_REMOTE)
    def _remove_port_event_handler(self, lport):
        LOG.debug('remove %(locality)s port: %(lport)s',
                  {'lport': lport,
                   'locality': 'local' if lport.is_local else 'remote'})
        # Let the router update process to delete flows for concrete
        # router port, if there is any.
        if lport.device_owner != common_const.DEVICE_OWNER_ROUTER_INTF:
            self._remove_port(lport)

    def _remove_port(self, lport):
        """Remove port which is not a router interface."""
        self._reprocess_to_delete_route(lport)

    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_LOCAL_UPDATED)
    @df_base_app.register_event(l2.LogicalPort, l2.EVENT_REMOTE_UPDATED)
    def _update_port_event_handler(self, lport, orig_lport):
        LOG.debug('remove %(locality)s port: %(lport)s',
                  {'lport': lport,
                   'locality': 'local' if lport.is_local else 'remote'})

        if lport.device_owner != common_const.DEVICE_OWNER_ROUTER_INTF:
            self._update_port(lport, orig_lport)

    def _update_port(self, lport, orig_lport):
        pass
